import { createActor, fromPromise } from 'xstate';
import { WL_PromiseCallback } from 'sdk/store/weila_datas';
import { loadWasmBinary } from 'sdk/audio/opus/opus_helper';
import playerWorklet from './player_worklet.js?url';
import { base64DecToArr } from 'sdk/audio/utils/utils.js';
import { buildPlayerInd, buildPlayerReq, PlayEvent } from './player_constant';
import CompoUint8Array from 'sdk/audio/utils/weila_compositor_array';
import { getOpusDataListFromPttData } from 'sdk/main/weila_utils';
import { player_machine } from './weila_player_fsm';


class WeilaPlayer {
    private coreActor: any;
    private audioNode: AudioWorkletNode | null;
    private audioBufferNode: AudioBufferSourceNode | null;
    private audioBuffer: AudioBuffer | null;
    private reqWaitingList: Map<number, WL_PromiseCallback>;
    private state: number;
    private playerId: number;
    private playEndCallback: ((id: number) => void) | null;

    constructor(private audioContext: any, private sampleRate: number, private id: number) {
        this.audioNode = null;
        this.audioBufferNode = null;
        this.audioBuffer = null;
        this.reqWaitingList = new Map();
        this.state = 0;
        this.playerId = id;
        this.playEndCallback = null;

        const m = player_machine.provide({
            actions: {
                player_stopped_entry: this.player_stopped_entry.bind(this),
                player_started_entry: this.player_started_entry.bind(this),
                player_paused_entry: this.player_paused_entry.bind(this),
                start_player_error: this.start_player_error.bind(this),
                start_player_done: this.start_player_done.bind(this),
                stop_player_done: this.stop_player_done.bind(this),
                stop_player_error: this.stop_player_error.bind(this),
                resume_player_done: this.resume_player_done.bind(this),
                pause_player_done: this.pause_player_done.bind(this),
                resume_player_error: this.resume_player_error.bind(this),
                pause_player_error: this.pause_player_error.bind(this),
                on_feed_data: this.on_feed_data.bind(this),
                unsupport_event: this.unsupport_event.bind(this),
            },
            actors: {
              start_player: fromPromise(this.start_player.bind(this)),
              stop_player: fromPromise(this.stop_player.bind(this)),
              pause_player: fromPromise(this.pause_player.bind(this)),
              resume_player: fromPromise(this.resume_player.bind(this)),
            }
        });

        this.coreActor = createActor(m);
        this.coreActor.subscribe((state: { value: any; context: any; }) => {
            console.log("WeilaPlayer state", state.value, state.context);
        })
        this.coreActor.start();
    }

    private player_stopped_entry(args: {context: any, event: any}) {
      console.log('player_stopped_entry', args);
    }
    private player_started_entry(args: {context: any, event: any}) {
      console.log('player_started_entry', args);
    }

    private player_paused_entry(args: {context: any, event: any}) {
      console.log('player_paused_entry', args);
    }

    private start_player_error(args: {context: any, event: any}) {
      console.log('start_player_error', args);
      const callback = args.event.error.param;
      if (callback && callback.reject) {
          callback.reject(args.event.error.error);
      }
    }

    private start_player_done(args: {context: any, event: any}) {
      console.log('start_player_done', args);
      const callback = args.event.output.param;
      if (callback && callback.resolve) {
          callback.resolve(args.event.output.result);
      }
    }

    private stop_player_done(args: {context: any, event: any}) {
      console.log('stop_player_done', args);
      const callback = args.event.output.param;
      if (callback && callback.resolve) {
          callback.resolve(args.event.output.result);
      }
    }

    private stop_player_error(args: {context: any, event: any}) {
      console.log('stop_player_error', args);
      const callback = args.event.error.param;
      if (callback && callback.reject) {
          callback.reject(args.event.error.error);
      }
    }

    private resume_player_done(args: {context: any, event: any}) {
      console.log('resume_player_done', args);
      const callback = args.event.output.param;
      if (callback && callback.resolve) {
          callback.resolve(args.event.output.result);
      }
    }

    private pause_player_done(args: {context: any, event: any}) {
      console.log('pause_player_done', args);
      const callback = args.event.output.param;
      if (callback && callback.resolve) {
          callback.resolve(args.event.output.result);
      }
    }

    private resume_player_error(args: {context: any, event: any}) {
      console.log('resume_player_error', args);
      const callback = args.event.error.param;
      if (callback && callback.reject) {
          callback.reject(args.event.error.error);
      }
    }

    private pause_player_error(args: {context: any, event: any}) {
      console.log('pause_player_error', args);
      const callback = args.event.error.param;
      if (callback && callback.reject) {
          callback.reject(args.event.error.error);
      }
    }

    private on_feed_data(args: {context: any, event: any}) {
      console.log('on_feed_data', args);
      const isLast = args.event.params.isLast;
      const opusFrameList = args.event.params.opusFrameList;

      if (isLast) {
          const ind = buildPlayerInd(PlayEvent.PLAYER_EVT_FEED, {isLast: isLast});
          this.audioNode!.port.postMessage(ind);
      }else {
          const compoArray = new CompoUint8Array();
          compoArray.putArrays(opusFrameList!);
          const compoBuffer = compoArray.getCompoArray();
          const ind = buildPlayerInd(PlayEvent.PLAYER_EVT_FEED, {isLast: false, buffer: compoBuffer, lenList: compoArray.getCompoLengthList()});
          this.audioNode!.port.postMessage(ind, [compoBuffer!.buffer]);
      }
    }
    private unsupport_event(args: {context: any, event: any}) {
      console.log('unsupport_event', args);
      throw new Error("unsupport event");
    }

    private onAudioWorkerMessage(event: MessageEvent) {
        console.log("weilaPlayer_onAudioNodeMessage", event.data.evt & 0x00FF, event.data.data);
        const evt = event.data.evt & 0x00FF;
        const data = event.data.data;
        if (evt == PlayEvent.PLAYER_EVT_PLAY_END) {
            if (this.playEndCallback) {
                this.playEndCallback(this.playerId);
            }
        }

        const callback = this.reqWaitingList.get(evt);
        console.log("weilaPlayer_onAudioNodeMessage", evt, callback);
        
        if (callback) {
            if (data.errCode == 0) {
                callback.resolve({result: true, param: callback.data});
            }else {
                callback.reject({error: data.errMsg, param: callback.data});
            }
            this.reqWaitingList.delete(evt);
        }
    }

    private async start_player(params: any) {
        console.log("start_player", params);
        const callback = params.input.param;

        try {
            await loadWasmBinary();
            const wasmBase64 = localStorage.getItem("WASMB");
            try {
                this.audioNode = new AudioWorkletNode(this.audioContext, "player_worklet", {
                    channelCount: 1,
                    processorOptions: {
                        sampleRate: 16000,
                        wasmData: base64DecToArr(wasmBase64),
                        id: this.playerId
                    }
                })                
            }catch (e) {
                await this.audioContext.audioWorklet.addModule(playerWorklet);
                this.audioNode = new AudioWorkletNode(this.audioContext, "player_worklet", {
                    channelCount: 1,
                    processorOptions: {
                        sampleRate: 16000,
                        wasmData: base64DecToArr(wasmBase64),
                        id: this.playerId
                    }
                })
            }

            this.audioNode.port.onmessage = this.onAudioWorkerMessage.bind(this);
            const openReq = buildPlayerReq(PlayEvent.PLAYER_EVT_OPEN, {sampleRate: 16000});
            this.audioNode.port.postMessage(openReq);

            try {
                await new Promise((resolve, reject) => {
                    const reqCallback = {} as WL_PromiseCallback;
                    reqCallback.resolve = resolve;
                    reqCallback.reject = reject;
                    this.reqWaitingList.set(PlayEvent.PLAYER_EVT_OPEN, reqCallback);
                    
                })

                console.log('open player successfully');
            }catch (e) {
                console.log('open player fail', e);
                return Promise.reject({error: e, ...params.input});
            }

            this.audioBuffer = this.audioContext.createBuffer(1, 16000, 16000);
            this.audioBufferNode = this.audioContext.createBufferSource();
            this.audioBufferNode!.buffer = this.audioBuffer;
            this.audioBufferNode!.loop = true;
            this.audioBufferNode!.connect(this.audioNode!);
            this.audioNode!.connect(this.audioContext.destination);
            this.audioBufferNode!.start();
    
            const startReq = buildPlayerReq(PlayEvent.PLAYER_EVT_START);
            this.audioNode!.port.postMessage(startReq);
            try {
                await new Promise((resolve, reject) => {
                    const reqCallback = {} as WL_PromiseCallback;
                    reqCallback.resolve = resolve;
                    reqCallback.reject = reject;
                    this.reqWaitingList.set(PlayEvent.PLAYER_EVT_START, reqCallback);
                })

                console.log('start player successfully');

                return ({result: true, ...params.input});
            }catch (e) {
                console.log('open player fail', e);
                const closeReq = buildPlayerReq(PlayEvent.PLAYER_EVT_CLOSE);
                this.audioNode!.port.postMessage(closeReq);                
                return Promise.reject({error: e, ...params.input});   
            }
        }catch (e) {
            console.log('open player fail', e);
            const closeReq = buildPlayerReq(PlayEvent.PLAYER_EVT_CLOSE);
            this.audioNode!.port.postMessage(closeReq);
            return Promise.reject({error: e, ...params.input});
        }
    }

    private async stop_player(params: any) {
        console.log("stop_player", params);
        try {
            const stopReq = buildPlayerReq(PlayEvent.PLAYER_EVT_STOP);
            this.audioNode!.port.postMessage(stopReq);
            await new Promise<boolean>((resolve, reject) => {
                const callback = {} as WL_PromiseCallback;
                callback.resolve = resolve;
                callback.reject = reject;
                this.reqWaitingList.set(PlayEvent.PLAYER_EVT_STOP, callback);
            })

            console.log('stop player successfully');
        }catch (e) {
            console.log('close player fail', e);
        }

        try {
            const closeReq = buildPlayerReq(PlayEvent.PLAYER_EVT_CLOSE);
            this.audioNode!.port.postMessage(closeReq);
            await new Promise<boolean>((resolve, reject) => {
                const callback = {} as WL_PromiseCallback;
                callback.resolve = resolve;
                callback.reject = reject;
                this.reqWaitingList.set(PlayEvent.PLAYER_EVT_CLOSE, callback);
            })

            console.log('close player successfully');
            
            return ({result: true, ...params.input});
        }catch (e) {
            console.log('close player fail', e);
            return Promise.reject({error: e, ...params.input});
        }
    }

    private async pause_player(params: any) {
        console.log("pause_player", params);
        const req = buildPlayerReq(PlayEvent.PLAYER_EVT_PAUSE);
        this.audioNode!.port.postMessage(req);
        try {
            await new Promise<boolean>((resolve, reject) => {
                const callback = {} as WL_PromiseCallback;
                callback.resolve = resolve;
                callback.reject = reject;
                this.reqWaitingList.set(PlayEvent.PLAYER_EVT_PAUSE, callback);
            })

            return ({result: true, ...params.input});
        }catch (e) {
            console.log(e);
            return Promise.reject({error: e, ...params.input});
        }
    }

    private async resume_player(params: any) {
        console.log("resume_player", params);
        const req = buildPlayerReq(PlayEvent.PLAYER_EVT_RESUME);
        this.audioNode!.port.postMessage(req);
        try {
            await new Promise<boolean>((resolve, reject) => {
                const callback = {} as WL_PromiseCallback;
                callback.resolve = resolve;
                callback.reject = reject;
                this.reqWaitingList.set(PlayEvent.PLAYER_EVT_RESUME, callback);
            })
            return ({result: true, ...params.input});
        }catch (e) {
            console.log(e);
            return Promise.reject({error: e, ...params.input});
        }
    }

    public async weilaPlayer_start(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this.coreActor.send({type: "start_play", param: {resolve, reject}});
        })
    }

    public async weilaPlayer_pause(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this.coreActor.send({type: "pause_play", param: {resolve, reject}});
        })
    }

    public async weilaPlayer_resume(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this.coreActor.send({type: "resume_play", param: {resolve, reject}});
        })
    }

    public async weilaPlayer_stop(): Promise<boolean> {
        return new Promise((resolve, reject) => {
            this.coreActor.send({type: "stop_play", param: {resolve, reject}});
        })
    }

    public weilaPlayer_setPlayEndListener(cb: (id: number) => void) {
        this.playEndCallback = cb;
    }

        /**
     * 喂送原始Opus帧列表数据
     * @param opusFrameList opus的帧列表
     * @param isLast 是否最后一个，如果true，后续会收到end的回调， 否则处于等待喂数据
     */
    public weilaPlayer_feedOpusFrameList(opusFrameList: Uint8Array[]|undefined, isLast: boolean) {
        this.coreActor.send({type: "feed_data", params: {opusFrameList, isLast}});
    }

    /**
     * 喂送weila的去头的音频数据，也可以是ptt的payload数据
     * @param audioBuffer weila去头的音频数据
     * @param isLast 是否最后一个，如果true，后续会收到end的回调， 否则处于等待喂数据
     */
    public weilaPlayer_feedWeilaAudioBuffer(audioBuffer: Uint8Array|null, isLast: boolean) {
        if (audioBuffer) {
            const opusFrameList = getOpusDataListFromPttData(audioBuffer);
            this.coreActor.send({type: "feed_data", params: {opusFrameList, isLast}});
        }else {
            this.coreActor.send({type: "feed_data", params: {opusFrameList: null, isLast}});
        }
    }
}

export {WeilaPlayer}